2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "CBPurpleAccount.h"
18 #import <libpurple/cmds.h>
19 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
20 #import <Adium/AIAccount.h>
21 #import <Adium/AIChat.h>
22 #import <Adium/AIContentMessage.h>
23 #import <Adium/AIContentNotification.h>
24 #import <Adium/AIHTMLDecoder.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIListGroup.h>
27 #import <Adium/AIListObject.h>
28 #import <Adium/AIMetaContact.h>
29 #import <Adium/AIService.h>
30 #import <Adium/AIServiceIcons.h>
31 #import <Adium/AIStatus.h>
32 #import <Adium/ESFileTransfer.h>
33 #import <Adium/AIWindowController.h>
34 #import <Adium/AIEmoticon.h>
35 #import <Adium/AIAccountControllerProtocol.h>
36 #import <Adium/AIChatControllerProtocol.h>
37 #import <Adium/AIContactControllerProtocol.h>
38 #import <Adium/AIContentControllerProtocol.h>
39 #import <Adium/AIInterfaceControllerProtocol.h>
40 #import <Adium/AIStatusControllerProtocol.h>
41 #import <Adium/AIPreferenceControllerProtocol.h>
42 #import <AIUtilities/AIAttributedStringAdditions.h>
43 #import <AIUtilities/AIDictionaryAdditions.h>
44 #import <AIUtilities/AIMenuAdditions.h>
45 #import <AIUtilities/AIMutableOwnerArray.h>
46 #import <AIUtilities/AIStringAdditions.h>
47 #import <AIUtilities/AIApplicationAdditions.h>
48 #import <AIUtilities/AIObjectAdditions.h>
49 #import <AIUtilities/AIImageAdditions.h>
50 #import <AIUtilities/AISystemNetworkDefaults.h>
51 #import "ESiTunesPlugin.h"
52 #import "AMPurpleTuneTooltip.h"
53 #import "adiumPurpleRequest.h"
55 #import "ESMSNService.h" //why oh why must the superclass know about MSN specific things!?
57 #define NO_GROUP @"__NoGroup__"
59 #define PREF_GROUP_ALIASES @"Aliases" //Preference group to store aliases in
60 #define NEW_ACCOUNT_DISPLAY_TEXT AILocalizedString(@"<New Account>", "Placeholder displayed as the name of a new account")
62 @interface CBPurpleAccount (PRIVATE)
63 - (NSString *)_mapIncomingGroupName:(NSString *)name;
64 - (NSString *)_mapOutgoingGroupName:(NSString *)name;
66 - (void)setTypingFlagOfChat:(AIChat *)inChat to:(NSNumber *)typingState;
68 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date;
69 - (void)_sentMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat toDestinationListContact:(AIListContact *)destinationContact flags:(PurpleMessageFlags)flags date:(NSDate *)date;
70 - (NSString *)_messageImageCachePathForID:(int)imageID;
72 - (ESFileTransfer *)createFileTransferObjectForXfer:(PurpleXfer *)xfer;
74 - (NSNumber *)shouldCheckMail;
76 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector;
77 - (void)continueConnectWithConfiguredPurpleAccount;
78 - (void)continueConnectWithConfiguredProxy;
79 - (void)continueRegisterWithConfiguredPurpleAccount;
80 - (void)promptForHostBeforeConnecting;
82 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation;
84 - (void)performAccountMenuAction:(NSMenuItem *)sender;
87 @implementation CBPurpleAccount
89 static SLPurpleCocoaAdapter *purpleThread = nil;
91 // The PurpleAccount currently associated with this Adium account
92 - (PurpleAccount*)purpleAccount
94 //Create a purple account if one does not already exist
96 [self createNewPurpleAccount];
97 AILog(@"%x: created PurpleAccount 0x%x with UID %@, protocolPlugin %s", [NSRunLoop currentRunLoop],account, [self UID], [self protocolPlugin]);
103 - (SLPurpleCocoaAdapter *)purpleThread
108 // Subclasses must override this
109 - (const char*)protocolPlugin { return NULL; }
111 // Contacts ------------------------------------------------------------------------------------------------
112 #pragma mark Contacts
113 - (void)newContact:(AIListContact *)theContact withName:(NSString *)inName
118 - (void)updateContact:(AIListContact *)theContact toGroupName:(NSString *)groupName contactName:(NSString *)contactName
120 //A quick sign on/sign off can leave these messages in the threaded messaging queue... we most definitely don't want
121 //to put the contact back into a remote group after signing off, as a ghost will appear. Spooky!
122 if ([self online] || [self integerStatusObjectForKey:@"Connecting"]) {
123 //When a new contact is created, if we aren't already silent and delayed, set it a second to cover our initial
125 if (!silentAndDelayed) {
126 [self silenceAllContactUpdatesForInterval:2.0];
127 [[adium contactController] delayListObjectNotificationsUntilInactivity];
130 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
131 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
132 if (![contactName isEqualToString:[theContact formattedUID]] && ![contactName isEqualToString:[theContact UID]]) {
133 [theContact setStatusObject:contactName
134 forKey:@"FormattedUID"
138 if (groupName && [groupName isEqualToString:@PURPLE_ORPHANS_GROUP_NAME]) {
139 [theContact setRemoteGroupName:AILocalizedString(@"Orphans","Name for the orphans group")];
140 } else if (groupName && [groupName length] != 0) {
141 [theContact setRemoteGroupName:[self _mapIncomingGroupName:groupName]];
143 AILog(@"Got a nil group for %@",theContact);
146 [self gotGroupForContact:theContact];
148 AILog(@"Got %@ for %@ while not online",groupName,theContact);
153 * @brief Change the UID of a contact
155 * If we're just passed a formatted version of the current UID, don't change the UID but instead use the information
156 * as the FormattedUID. For example, we get sent this when an AIM contact's name formatting changes; we always want
157 * to use a lowercase and space-free version for the UID, however.
159 - (void)renameContact:(AIListContact *)theContact toUID:(NSString *)newUID
161 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
162 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
163 NSString *filteredUID = [[self service] filterUID:newUID removeIgnoredCharacters:YES];
165 if ([filteredUID isEqualToString:[theContact UID]]) {
166 [theContact setStatusObject:newUID
167 forKey:@"FormattedUID"
170 [theContact setUID:newUID];
174 - (void)updateContact:(AIListContact *)theContact toAlias:(NSString *)purpleAlias
176 if (![[purpleAlias compactedString] isEqualToString:[[theContact UID] compactedString]]) {
177 //Store this alias as the serverside display name so long as it isn't identical when unformatted to the UID
178 [theContact setServersideAlias:purpleAlias
179 asStatusMessage:[self useDisplayNameAsStatusMessage]
180 silently:silentAndDelayed];
183 //If it's the same characters as the UID, apply it as a formatted UID
184 if (![purpleAlias isEqualToString:[theContact formattedUID]] &&
185 ![purpleAlias isEqualToString:[theContact UID]]) {
186 [theContact setFormattedUID:purpleAlias
190 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
195 - (BOOL)useDisplayNameAsStatusMessage
200 - (void)updateContact:(AIListContact *)theContact forEvent:(NSNumber *)event
206 - (void)updateSignon:(AIListContact *)theContact withData:(void *)data
208 [theContact setOnline:YES
210 silently:silentAndDelayed];
212 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
216 - (void)updateSignoff:(AIListContact *)theContact withData:(void *)data
218 [theContact setOnline:NO
220 silently:silentAndDelayed];
222 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
226 - (void)updateSignonTime:(AIListContact *)theContact withData:(NSDate *)signonDate
228 [theContact setSignonDate:signonDate
232 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
236 * @brief Status name to use for a Purple buddy
238 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
244 * @brief Status message for a contact
246 - (NSAttributedString *)statusMessageForPurpleBuddy:(PurpleBuddy *)buddy
248 PurplePresence *presence = purple_buddy_get_presence(buddy);
249 PurpleStatus *status = (presence ? purple_presence_get_active_status(presence) : NULL);
250 const char *message = (status ? purple_status_get_attr_string(status, "message") : NULL);
252 return (message ? [AIHTMLDecoder decodeHTML:[NSString stringWithUTF8String:message]] : nil);
256 * @brief Update the status message and away state of the contact
258 - (void)updateStatusForContact:(AIListContact *)theContact toStatusType:(NSNumber *)statusTypeNumber statusName:(NSString *)statusName statusMessage:(NSAttributedString *)statusMessage
260 [theContact setStatusWithName:statusName
261 statusType:[statusTypeNumber intValue]
263 [theContact setStatusMessage:statusMessage
267 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
271 - (void)updateWentIdle:(AIListContact *)theContact withData:(NSDate *)idleSinceDate
273 [theContact setIdle:YES sinceDate:idleSinceDate notify:NotifyLater];
276 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
278 - (void)updateIdleReturn:(AIListContact *)theContact withData:(void *)data
280 [theContact setIdle:NO
285 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
288 //Evil level (warning level)
289 - (void)updateEvil:(AIListContact *)theContact withData:(NSNumber *)evilNumber
291 [theContact setWarningLevel:[evilNumber intValue]
295 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
299 - (void)updateIcon:(AIListContact *)theContact withData:(NSData *)userIconData
301 [theContact setServersideIconData:userIconData
305 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
308 - (void)updateMobileStatus:(AIListContact *)theContact withData:(BOOL)isMobile
310 [theContact setIsMobile:isMobile notify:NotifyLater];
312 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
315 - (NSString *)processedIncomingUserInfo:(NSString *)inString
317 NSMutableString *returnString = nil;
318 if ([inString rangeOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."].location != NSNotFound) {
319 returnString = [[inString mutableCopy] autorelease];
320 [returnString replaceOccurrencesOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."
321 withString:AILocalizedString(@"Adium could not find any information in the user's profile. This may not be a registered name.", "Message shown when a contact's profile can't be found")
322 options:NSLiteralSearch
323 range:NSMakeRange(0, [returnString length])];
326 return (returnString ? returnString : inString);
330 - (void)updateUserInfo:(AIListContact *)theContact withData:(PurpleNotifyUserInfo *)user_info
332 char *user_info_text = purple_notify_user_info_get_text_with_newline(user_info, "<BR />");
333 NSMutableString *mutablePurpleUserInfo = (user_info_text ? [NSMutableString stringWithUTF8String:user_info_text] : nil);
334 g_free(user_info_text);
336 //Libpurple may pass us HTML with embedded </html> tags. Yuck. Don't abort when we hit one in AIHTMLDecoder.
337 [mutablePurpleUserInfo replaceOccurrencesOfString:@"</html>"
339 options:(NSCaseInsensitiveSearch | NSLiteralSearch)
340 range:NSMakeRange(0, [mutablePurpleUserInfo length])];
342 NSString *purpleUserInfo = mutablePurpleUserInfo;
343 purpleUserInfo = processPurpleImages(purpleUserInfo, self);
344 purpleUserInfo = [self processedIncomingUserInfo:purpleUserInfo];
346 [theContact setProfile:[AIHTMLDecoder decodeHTML:purpleUserInfo]
350 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
354 * @brief Purple removed a contact from the local blist
356 * This can happen in many situations:
357 * - For every contact on an account when the account signs off
358 * - For a contact as it is deleted by the user
359 * - For a contact as it is deleted by Purple (e.g. when Sametime refuses an addition because it is known to be invalid)
360 * - In the middle of the move process as a contact moves from one group to another
362 * We need not take any action; we'll be notified of changes by Purple as necessary.
364 - (void)removeContact:(AIListContact *)theContact
369 //To allow root level buddies on protocols which don't support them, we map any buddies in a group
370 //named after this account's UID to the root group. These functions handle the mapping. Group names should
371 //be filtered through incoming before being sent to Adium - and group names from Adium should be filtered through
372 //outgoing before being used.
373 - (NSString *)_mapIncomingGroupName:(NSString *)name
375 if (!name || ([[name compactedString] caseInsensitiveCompare:[self UID]] == NSOrderedSame)) {
376 return ADIUM_ROOT_GROUP_NAME;
381 - (NSString *)_mapOutgoingGroupName:(NSString *)name
383 if ([[name compactedString] caseInsensitiveCompare:ADIUM_ROOT_GROUP_NAME] == NSOrderedSame) {
390 //Update the status of a contact (Request their profile)
391 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
394 [purpleThread getInfoFor:[inContact UID] onAccount:self];
397 - (void)requestAddContactWithUID:(NSString *)contactUID
399 [[adium contactController] requestAddContactWithUID:contactUID
400 service:[self _serviceForUID:contactUID]
404 - (AIService *)_serviceForUID:(NSString *)contactUID
406 return [self service];
409 - (void)gotGroupForContact:(AIListContact *)listContact {};
411 /*********************/
412 /* AIAccount_Handles */
413 /*********************/
414 #pragma mark Contact List Editing
416 - (void)removeContacts:(NSArray *)objects
418 NSEnumerator *enumerator = [objects objectEnumerator];
419 AIListContact *object;
421 while ((object = [enumerator nextObject])) {
422 NSString *groupName = [self _mapOutgoingGroupName:[object remoteGroupName]];
424 //Have the purple thread perform the serverside actions
425 [purpleThread removeUID:[object UID] onAccount:self fromGroup:groupName];
427 //Remove it from Adium's list
428 [object setRemoteGroupName:nil];
432 - (void)addContacts:(NSArray *)objects toGroup:(AIListGroup *)group
434 NSEnumerator *enumerator = [objects objectEnumerator];
435 AIListContact *object;
436 NSString *groupName = [self _mapOutgoingGroupName:[group UID]];
438 while ((object = [enumerator nextObject])) {
439 [purpleThread addUID:[self _UIDForAddingObject:object] onAccount:self toGroup:groupName];
441 //Add it to Adium's list
442 [object setRemoteGroupName:[group UID]]; //Use the non-mapped group name locally
446 - (NSString *)_UIDForAddingObject:(AIListContact *)object
451 - (void)moveListObjects:(NSArray *)objects toGroup:(AIListGroup *)group
453 NSString *groupName = [self _mapOutgoingGroupName:[group UID]];
454 NSEnumerator *enumerator;
455 AIListContact *listObject;
457 //Move the objects to it
458 enumerator = [objects objectEnumerator];
459 while ((listObject = [enumerator nextObject])) {
460 if ([listObject isKindOfClass:[AIListGroup class]]) {
461 //Since no protocol here supports nesting, a group move is really a re-name
464 // NSString *oldGroupName = [self _mapOutgoingGroupName:[listObject remoteGroupName]];
466 //Tell the purple thread to perform the serverside operation
467 [purpleThread moveUID:[listObject UID] onAccount:self toGroup:groupName];
469 //Use the non-mapped group name locally
470 [listObject setRemoteGroupName:[group UID]];
475 - (void)renameGroup:(AIListGroup *)inGroup to:(NSString *)newName
477 NSString *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
479 //Tell the purple thread to perform the serverside operation
480 [purpleThread renameGroup:groupName onAccount:self to:newName];
482 //We must also update the remote grouping of all our contacts in that group
483 NSEnumerator *enumerator = [[[adium contactController] allContactsInObject:inGroup recurse:YES onAccount:self] objectEnumerator];
484 AIListContact *contact;
486 while ((contact = [enumerator nextObject])) {
487 //Evan: should we use groupName or newName here?
488 [contact setRemoteGroupName:newName];
492 - (void)deleteGroup:(AIListGroup *)inGroup
494 NSString *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
496 [purpleThread deleteGroup:groupName onAccount:self];
499 // Return YES if the contact list is editable
500 - (BOOL)contactListEditable
502 return [self online];
505 - (id)authorizationRequestWithDict:(NSDictionary*)dict {
506 return [[[AIObject sharedAdiumInstance] contactController] showAuthorizationRequestWithDict:dict
510 - (void)authorizationWindowController:(NSWindowController *)inWindowController authorizationWithDict:(NSDictionary *)infoDict didAuthorize:(BOOL)inDidAuthorize
515 if (inDidAuthorize) {
516 callback = [[[infoDict objectForKey:@"authorizeCB"] retain] autorelease];
518 callback = [[[infoDict objectForKey:@"denyCB"] retain] autorelease];
521 [purpleThread doAuthRequestCbValue:callback withUserDataValue:[[[infoDict objectForKey:@"userData"] retain] autorelease]];
525 //Chats ------------------------------------------------------------
529 * @brief Called by Purple code when a chat should be opened by the interface
531 * If the user sent an initial message, this will be triggered and have no effect.
533 * If a remote user sent an initial message, however, a chat will be created without being opened. This call is our
534 * cue to actually open chat.
536 * Another situation in which this is relevant is when we request joining a group chat; the chat should only be actually
537 * opened once the server notifies us that we are in the room.
539 * This will ultimately call -[CBPurpleAccount openChat:] below if the chat was not previously open.
541 - (void)addChat:(AIChat *)chat
543 AILogWithSignature(@"");
546 [[adium interfaceController] openChat:chat];
548 [chat accountDidJoinChat];
551 //Open a chat for Adium
552 - (BOOL)openChat:(AIChat *)chat
554 /* The #if 0'd block below causes crashes in msn_tooltip_text() on MSN */
556 AIListContact *listContact;
558 //Obtain the contact's information if it's a stranger
559 if ((listContact = [chat listObject]) && ([listContact isStranger])) {
560 [self delayedUpdateContactStatus:listContact];
564 AILog(@"purple openChat:%@ for %@",chat,[chat uniqueChatID]);
566 //Inform purple that we have opened this chat
567 [purpleThread openChat:chat onAccount:self];
569 //Created the chat successfully
573 - (BOOL)closeChat:(AIChat*)chat
575 [purpleThread closeChat:chat];
577 //Be sure any remaining typing flag is cleared as the chat closes
578 [self setTypingFlagOfChat:chat to:nil];
579 AILog(@"purple closeChat:%@",[chat uniqueChatID]);
584 - (AIChat *)chatWithContact:(AIListContact *)contact identifier:(id)identifier
586 AIChat *chat = [[adium chatController] chatWithContact:contact];
587 [chat setIdentifier:identifier];
593 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier
595 return [[adium chatController] chatWithName:name identifier:identifier onAccount:self chatCreationInfo:nil];
598 //Typing update in an IM
599 - (void)typingUpdateForIMChat:(AIChat *)chat typing:(NSNumber *)typingState
601 [self setTypingFlagOfChat:chat
605 //Multiuser chat update
606 - (void)convUpdateForChat:(AIChat *)chat type:(NSNumber *)type
612 * @brief Called when we are informed that we left a multiuser chat
614 - (void)leftChat:(AIChat *)chat
616 AILogWithSignature(@"Chat left - something should happen here!");
619 - (void)updateTopic:(NSString *)inTopic forChat:(AIChat *)chat
623 - (void)updateTitle:(NSString *)inTitle forChat:(AIChat *)chat
625 [[chat displayArrayForKey:@"Display Name"] setObject:inTitle
629 - (void)updateForChat:(AIChat *)chat type:(NSNumber *)type
631 AIChatUpdateType updateType = [type intValue];
633 switch (updateType) {
635 case AIChatClosedWindow:
640 [chat setStatusObject:[NSNumber numberWithBool:YES] forKey:key notify:NotifyNow];
641 [chat setStatusObject:nil forKey:key notify:NotifyNever];
646 - (void)errorForChat:(AIChat *)chat type:(NSNumber *)type
648 [chat receivedError:type];
651 - (void)receivedIMChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
653 PurpleMessageFlags flags = [[messageDict objectForKey:@"PurpleMessageFlags"] intValue];
655 if ((flags & PURPLE_MESSAGE_SEND) != 0) {
656 //Purple is telling us that our message was sent successfully.
658 //We would tell the other side that we're done typing, except that if we do this now, the typing notification icon in some clients (e.g., iChat) disappears before the message actually arrives.
659 //[purpleThread sendTyping:AINotTyping inChat:chat];
661 NSAttributedString *attributedMessage;
662 AIListContact *listContact;
664 listContact = [chat listObject];
666 attributedMessage = [[adium contentController] decodedIncomingMessage:[messageDict objectForKey:@"Message"]
667 fromContact:listContact
670 //Clear the typing flag of the chat since a message was just received
671 [self setTypingFlagOfChat:chat to:nil];
673 [self _receivedMessage:attributedMessage
675 fromListContact:listContact
677 date:[messageDict objectForKey:@"Date"]];
681 - (void)receivedMultiChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
683 PurpleMessageFlags flags = [[messageDict objectForKey:@"PurpleMessageFlags"] intValue];
684 NSAttributedString *attributedMessage = [messageDict objectForKey:@"AttributedMessage"];;
685 NSString *source = [messageDict objectForKey:@"Source"];
688 [self _receivedMessage:attributedMessage
690 fromListContact:[self contactWithUID:source]
692 date:[messageDict objectForKey:@"Date"]];
694 //If we didn't get a listContact, this is a purple status message... display it as such.
695 #warning need to translate the type here
696 [[adium contentController] displayEvent:[attributedMessage string]
703 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date
705 AIContentMessage *messageObject = [AIContentMessage messageInChat:chat
706 withSource:sourceContact
709 message:attributedMessage
710 autoreply:(flags & PURPLE_MESSAGE_AUTO_RESP) != 0];
712 [[adium contentController] receiveContentObject:messageObject];
715 /*********************/
716 /* AIAccount_Content */
717 /*********************/
719 - (void)sendTypingObject:(AIContentTyping *)inContentTyping
721 AIChat *chat = [inContentTyping chat];
723 if (![chat isGroupChat]) {
724 [purpleThread sendTyping:[inContentTyping typingState] inChat:chat];
728 - (BOOL)supportsSendingNotifications
730 return (account ? ((PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(account)))->send_attention) != NULL) : NO);
733 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
735 PurpleMessageFlags flags = PURPLE_MESSAGE_RAW;
737 if ([inContentMessage isKindOfClass:[AIContentNotification class]] &&
738 [self supportsSendingNotifications]) {
739 //Send a notification directly if possible
740 [purpleThread sendNotificationOfType:[(AIContentNotification *)inContentMessage notificationType]
742 inChat:[inContentMessage chat]];
745 if ([inContentMessage isAutoreply]) {
746 flags |= PURPLE_MESSAGE_AUTO_RESP;
749 [purpleThread sendEncodedMessage:[inContentMessage encodedMessage]
751 inChat:[inContentMessage chat]
759 * @brief Return the string encoded for sending to a remote contact
761 * We return nil if the string turns out to have been a / command.
763 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
765 NSString *encodedString;
766 BOOL didCommand = [purpleThread attemptPurpleCommandOnMessage:[[inContentMessage message] string]
767 fromAccount:(AIAccount *)[inContentMessage source]
768 inChat:[inContentMessage chat]];
770 encodedString = (didCommand ?
772 [super encodedAttributedStringForSendingContentMessage:inContentMessage]);
774 return encodedString;
778 * @brief Allow newlines in messages
780 * Only IRC doesn't allow newlines out of the built-in prpls... and we don't even support it yet.
781 * This method is never called at present.
783 - (BOOL)allowsNewlinesInMessages
785 return (account && account->gc && ((account->gc->flags & PURPLE_CONNECTION_NO_NEWLINES) != 0));
789 * @brief Libpurple prints file transfer messages to the chat window. The Adium core therefore shouldn't.
791 - (BOOL)accountDisplaysFileTransferMessages
796 //Return YES if we're available for sending the specified content or will be soon (are currently connecting).
797 //If inListObject is nil, we can return YES if we will 'most likely' be able to send the content.
798 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact
800 BOOL weAreOnline = [self online];
802 if ([inType isEqualToString:CONTENT_MESSAGE_TYPE] ||
803 [inType isEqualToString:CONTENT_NOTIFICATION_TYPE]) {
804 if ((weAreOnline && (inContact == nil || [inContact online])) ||
805 ([self integerStatusObjectForKey:@"Connecting"])) { //XXX - Why do we lie if we're connecting? -ai
808 } else if (([inType isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) && ([self conformsToProtocol:@protocol(AIAccount_Files)])) {
811 if ([inContact online]) {
812 return [self allowFileTransferWithListObject:inContact];
823 - (BOOL)allowFileTransferWithListObject:(AIListObject *)inListObject
825 PurplePluginProtocolInfo *prpl_info = NULL;
827 if (account && account->gc && account->gc->prpl)
828 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
830 if (prpl_info && prpl_info->send_file)
831 return (!prpl_info->can_receive_file || prpl_info->can_receive_file(account->gc, [[inListObject UID] UTF8String]));
836 - (BOOL)supportsAutoReplies
838 if (account && account->gc) {
839 return ((account->gc->flags & PURPLE_CONNECTION_AUTO_RESP) != 0);
845 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
847 PurplePluginProtocolInfo *prpl_info = NULL;
849 if (account && account->gc && account->gc->prpl)
850 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
852 if (prpl_info && prpl_info->offline_message) {
854 return (prpl_info->offline_message(purple_find_buddy(account, [[inContact UID] UTF8String])));
861 #pragma mark Custom emoticons
862 - (void)chat:(AIChat *)inChat isWaitingOnCustomEmoticon:(NSString *)emoticonEquivalent
864 if(![[[adium preferenceController] preferenceForKey:KEY_MSN_DISPLAY_CUSTOM_EMOTICONS
865 group:PREF_GROUP_MSN_SERVICE] boolValue])
867 AIEmoticon *emoticon;
869 //Look for an existing emoticon with this equivalent
870 NSEnumerator *enumerator = [[inChat customEmoticons] objectEnumerator];
871 while ((emoticon = [enumerator nextObject])) {
872 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
876 emoticon = [AIEmoticon emoticonWithIconPath:nil
877 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
878 name:emoticonEquivalent
880 [inChat addCustomEmoticon:emoticon];
883 if (![emoticon path]) {
884 [emoticon setPath:[[NSBundle bundleForClass:[CBPurpleAccount class]] pathForResource:@"missing_image"
890 * @brief Return the path at which to save an emoticon
892 - (NSString *)_emoticonCachePathForEmoticon:(NSString *)emoticonEquivalent type:(AIBitmapImageFileType)fileType inChat:(AIChat *)inChat
894 static unsigned long long emoticonID = 0;
895 NSString *filename = [NSString stringWithFormat:@"TEMP-CustomEmoticon_%@_%@_%qu.%@",
896 [inChat uniqueChatID], emoticonEquivalent, emoticonID++, [NSImage extensionForBitmapImageFileType:fileType]];
897 return [[adium cachesPath] stringByAppendingPathComponent:[filename safeFilenameString]];
901 - (void)chat:(AIChat *)inChat setCustomEmoticon:(NSString *)emoticonEquivalent withImageData:(NSData *)inImageData
903 if(![[[adium preferenceController] preferenceForKey:KEY_MSN_DISPLAY_CUSTOM_EMOTICONS
904 group:PREF_GROUP_MSN_SERVICE] boolValue])
906 /* XXX Note: If we can set outgoing emoticons, this method needs to be updated to mark emoticons as incoming
907 * and AIEmoticonController needs to be able to handle that.
909 AIEmoticon *emoticon;
911 //Look for an existing emoticon with this equivalent
912 NSEnumerator *enumerator = [[inChat customEmoticons] objectEnumerator];
913 while ((emoticon = [enumerator nextObject])) {
914 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
917 //Write out our image
918 NSString *path = [self _emoticonCachePathForEmoticon:emoticonEquivalent
919 type:[NSImage fileTypeOfData:inImageData]
921 [inImageData writeToFile:path
925 //If we already have an emoticon, just update its path
926 [emoticon setPath:path];
929 emoticon = [AIEmoticon emoticonWithIconPath:path
930 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
931 name:emoticonEquivalent
933 [inChat addCustomEmoticon:emoticon];
937 - (void)chat:(AIChat *)inChat closedCustomEmoticon:(NSString *)emoticonEquivalent
939 if(![[[adium preferenceController] preferenceForKey:KEY_MSN_DISPLAY_CUSTOM_EMOTICONS
940 group:PREF_GROUP_MSN_SERVICE] boolValue])
942 AIEmoticon *emoticon;
944 //Look for an existing emoticon with this equivalent
945 NSEnumerator *enumerator = [[inChat customEmoticons] objectEnumerator];
946 while ((emoticon = [enumerator nextObject])) {
947 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
951 [[adium notificationCenter] postNotificationName:@"AICustomEmoticonUpdated"
953 userInfo:[NSDictionary dictionaryWithObject:emoticon
954 forKey:@"AIEmoticon"]];
956 //This shouldn't happen; chat:setCustomEmoticon:withImageData: should have already been called.
957 emoticon = [AIEmoticon emoticonWithIconPath:nil
958 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
959 name:emoticonEquivalent
961 NSLog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
962 AILog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
966 #pragma mark PurpleConversation User Lists
967 - (NSString *)uidForContactWithUID:(NSString *)inUID inChat:(AIChat *)chat
969 //No change for the superclass; subclasses may wish to modify it
972 - (void)addUsersArray:(NSArray *)usersArray
973 withFlags:(NSArray *)flagsArray
974 andAliases:(NSArray *)aliasesArray
975 newArrivals:(NSNumber *)newArrivals
976 toChat:(AIChat *)chat
979 BOOL isNewArrival = (newArrivals && [newArrivals boolValue]);
981 AILog(@"*** %@: addUsersArray:%@ toChat:%@",self,usersArray,chat);
983 count = [usersArray count];
984 for (i = 0; i < count; i++) {
985 NSString *contactName;
987 AIListContact *listContact;
988 PurpleConvChatBuddyFlags flags;
990 contactName = [usersArray objectAtIndex:i];
991 flags = [[flagsArray objectAtIndex:i] intValue];
992 alias = [aliasesArray objectAtIndex:i];
994 listContact = [self contactWithUID:[self uidForContactWithUID:contactName inChat:chat]];
995 [listContact setStatusObject:contactName forKey:@"FormattedUID" notify:NotifyNow];
997 if (alias && [alias length]) {
998 [listContact setServersideAlias:alias asStatusMessage:NO silently:YES];
1001 [chat addParticipatingListObject:listContact notify:isNewArrival];
1005 - (void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
1007 AIListContact *contact;
1010 (contact = [self contactWithUID:[self uidForContactWithUID:contactName inChat:chat]])) {
1012 [chat removeObject:contact];
1014 AILog(@"%@ removeUser:%@ fromChat:%@",self,contact,chat);
1016 AILog(@"Could not remove %@ from %@ (contactWithUID: %@)",
1017 contactName,chat,[self contactWithUID:[self uidForContactWithUID:contactName inChat:chat]]);
1021 - (void)removeUsersArray:(NSArray *)usersArray fromChat:(AIChat *)chat
1023 NSEnumerator *enumerator = [usersArray objectEnumerator];
1024 NSString *contactName;
1025 while ((contactName = [enumerator nextObject])) {
1026 [self removeUser:contactName fromChat:chat];
1030 /*********************/
1031 /* AIAccount_Privacy */
1032 /*********************/
1033 #pragma mark Privacy
1034 - (BOOL)addListObject:(AIListObject *)inObject toPrivacyList:(AIPrivacyType)type
1036 if (type == AIPrivacyTypePermit)
1037 return (purple_privacy_permit_add(account,[[inObject UID] UTF8String],FALSE));
1039 return (purple_privacy_deny_add(account,[[inObject UID] UTF8String],FALSE));
1042 - (BOOL)removeListObject:(AIListObject *)inObject fromPrivacyList:(AIPrivacyType)type
1044 if (type == AIPrivacyTypePermit)
1045 return (purple_privacy_permit_remove(account,[[inObject UID] UTF8String],FALSE));
1047 return (purple_privacy_deny_remove(account,[[inObject UID] UTF8String],FALSE));
1050 - (NSArray *)listObjectsOnPrivacyList:(AIPrivacyType)type
1052 NSMutableArray *array = [NSMutableArray array];
1055 GSList *sourceList = ((type == AIPrivacyTypePermit) ? account->permit : account->deny);
1057 for (list = sourceList; (list != NULL); list=list->next) {
1058 [array addObject:[self contactWithUID:[NSString stringWithUTF8String:(char *)list->data]]];
1065 - (void)accountPrivacyList:(AIPrivacyType)type added:(NSString *)sourceUID
1067 //Can't really trust sourceUID to not be @"" or something silly like that
1068 if ([sourceUID length]) {
1070 AIListContact *contact = [self contactWithUID:sourceUID];
1072 //Update Adium's knowledge of it
1073 [contact setIsBlocked:((type == AIPrivacyTypeDeny) ? YES : NO) updateList:NO];
1077 - (void)privacyPermitListAdded:(NSString *)sourceUID
1079 [self accountPrivacyList:AIPrivacyTypePermit added:sourceUID];
1082 - (void)privacyDenyListAdded:(NSString *)sourceUID
1084 [self accountPrivacyList:AIPrivacyTypeDeny added:sourceUID];
1087 - (void)accountPrivacyList:(AIPrivacyType)type removed:(NSString *)sourceUID
1089 //Can't really trust sourceUID to not be @"" or something silly like that
1090 if ([sourceUID length]) {
1091 if (!namesAreCaseSensitive) {
1092 sourceUID = [sourceUID compactedString];
1095 //Get our contact, which must already exist for us to care about its removal
1096 AIListContact *contact = [[adium contactController] existingContactWithService:service
1101 //Update Adium's knowledge of it
1102 [contact setIsBlocked:((type == AIPrivacyTypeDeny) ? NO : YES) updateList:NO];
1107 - (void)privacyPermitListRemoved:(NSString *)sourceUID
1109 [self accountPrivacyList:AIPrivacyTypePermit removed:sourceUID];
1112 - (void)privacyDenyListRemoved:(NSString *)sourceUID
1114 [self accountPrivacyList:AIPrivacyTypeDeny removed:sourceUID];
1117 - (void)setPrivacyOptions:(AIPrivacyOption)option
1119 if (account && purple_account_get_connection(account)) {
1120 PurplePrivacyType privacyType;
1123 case AIPrivacyOptionAllowAll:
1125 privacyType = PURPLE_PRIVACY_ALLOW_ALL;
1127 case AIPrivacyOptionDenyAll:
1128 privacyType = PURPLE_PRIVACY_DENY_ALL;
1130 case AIPrivacyOptionAllowUsers:
1131 privacyType = PURPLE_PRIVACY_ALLOW_USERS;
1133 case AIPrivacyOptionDenyUsers:
1134 privacyType = PURPLE_PRIVACY_DENY_USERS;
1136 case AIPrivacyOptionAllowContactList:
1137 privacyType = PURPLE_PRIVACY_ALLOW_BUDDYLIST;
1141 account->perm_deny = privacyType;
1142 serv_set_permit_deny(purple_account_get_connection(account));
1143 AILog(@"Set privacy options for %@ (%x %x) to %i",
1144 self,account,purple_account_get_connection(account),account->perm_deny);
1146 AILog(@"Couldn't set privacy options for %@ (%x %x)",self,account,purple_account_get_connection(account));
1150 - (AIPrivacyOption)privacyOptions
1152 AIPrivacyOption privacyOption = -1;
1155 PurplePrivacyType privacyType = account->perm_deny;
1157 switch (privacyType) {
1158 case PURPLE_PRIVACY_ALLOW_ALL:
1160 privacyOption = AIPrivacyOptionAllowAll;
1162 case PURPLE_PRIVACY_DENY_ALL:
1163 privacyOption = AIPrivacyOptionDenyAll;
1165 case PURPLE_PRIVACY_ALLOW_USERS:
1166 privacyOption = AIPrivacyOptionAllowUsers;
1168 case PURPLE_PRIVACY_DENY_USERS:
1169 privacyOption = AIPrivacyOptionDenyUsers;
1171 case PURPLE_PRIVACY_ALLOW_BUDDYLIST:
1172 privacyOption = AIPrivacyOptionAllowContactList;
1176 AILog(@"%@: privacyOptions are %i",self,privacyOption);
1177 return privacyOption;
1180 /*****************************************************/
1181 /* File transfer / AIAccount_Files inherited methods */
1182 /*****************************************************/
1183 #pragma mark File Transfer
1184 - (BOOL)canSendFolders
1189 //Create a protocol-specific xfer object, set it up as requested, and begin sending
1190 - (void)_beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
1192 PurpleXfer *xfer = [self newOutgoingXferForFileTransfer:fileTransfer];
1195 //Associate the fileTransfer and the xfer with each other
1196 [fileTransfer setAccountData:[NSValue valueWithPointer:xfer]];
1197 xfer->ui_data = [fileTransfer retain];
1200 purple_xfer_set_local_filename(xfer, [[fileTransfer localFilename] UTF8String]);
1201 purple_xfer_set_filename(xfer, [[[fileTransfer localFilename] lastPathComponent] UTF8String]);
1204 Request that the transfer begins.
1205 We will be asked to accept it via:
1206 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1209 [purpleThread xferRequest:xfer];
1210 [fileTransfer setStatus: Waiting_on_Remote_User_FileTransfer];
1213 //By default, protocols can not create PurpleXfer objects
1214 - (PurpleXfer *)newOutgoingXferForFileTransfer:(ESFileTransfer *)fileTransfer
1216 PurpleXfer *newPurpleXfer = NULL;
1218 if (account && purple_account_get_connection(account)) {
1220 PurplePluginProtocolInfo *prpl_info = ((prpl = purple_find_prpl(account->protocol_id)) ?
1221 PURPLE_PLUGIN_PROTOCOL_INFO(prpl) :
1224 if (prpl_info && prpl_info->new_xfer) {
1225 char *destsn = (char *)[[[fileTransfer contact] UID] UTF8String];
1226 newPurpleXfer = (prpl_info->new_xfer)(purple_account_get_connection(account), destsn);
1230 return newPurpleXfer;
1234 * @brief The account requested that we received a file.
1236 * Set up the ESFileTransfer and query the fileTransferController for a save location.
1239 - (void)requestReceiveOfFileTransfer:(ESFileTransfer *)fileTransfer
1241 AILog(@"File transfer request received: %@",fileTransfer);
1242 [[adium fileTransferController] receiveRequestForFileTransfer:fileTransfer];
1245 //Create an ESFileTransfer object from an xfer
1246 - (ESFileTransfer *)newFileTransferObjectWith:(NSString *)destinationUID
1247 size:(unsigned long long)inSize
1248 remoteFilename:(NSString *)remoteFilename
1250 AIListContact *contact = [self contactWithUID:destinationUID];
1251 ESFileTransfer *fileTransfer;
1253 fileTransfer = [[adium fileTransferController] newFileTransferWithContact:contact
1255 type:Unknown_FileTransfer];
1256 [fileTransfer setSize:inSize];
1257 [fileTransfer setRemoteFilename:remoteFilename];
1259 return fileTransfer;
1262 //Update an ESFileTransfer object progress
1263 - (void)updateProgressForFileTransfer:(ESFileTransfer *)fileTransfer percent:(NSNumber *)percent bytesSent:(NSNumber *)bytesSent
1265 float percentDone = [percent floatValue];
1266 [fileTransfer setPercentDone:percentDone bytesSent:[bytesSent unsignedLongValue]];
1269 //The local side cancelled the transfer. We probably already have this status set, but set it just in case.
1270 - (void)fileTransferCancelledLocally:(ESFileTransfer *)fileTransfer
1272 if (![fileTransfer isStopped]) {
1273 [fileTransfer setStatus:Cancelled_Local_FileTransfer];
1277 //The remote side cancelled the transfer, the fool. Update our status.
1278 - (void)fileTransferCancelledRemotely:(ESFileTransfer *)fileTransfer
1280 if (![fileTransfer isStopped]) {
1281 [fileTransfer setStatus:Cancelled_Remote_FileTransfer];
1285 - (void)destroyFileTransfer:(ESFileTransfer *)fileTransfer
1287 AILog(@"Destroy file transfer %@",fileTransfer);
1288 [fileTransfer release];
1291 //Accept a send or receive ESFileTransfer object, beginning the transfer.
1292 //Subsequently inform the fileTransferController that the fun has begun.
1293 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1295 AILog(@"Accepted file transfer %@",fileTransfer);
1298 PurpleXferType xferType;
1300 xfer = [[fileTransfer accountData] pointerValue];
1302 xferType = purple_xfer_get_type(xfer);
1303 if ( xferType == PURPLE_XFER_SEND ) {
1304 [fileTransfer setFileTransferType:Outgoing_FileTransfer];
1305 } else if ( xferType == PURPLE_XFER_RECEIVE ) {
1306 [fileTransfer setFileTransferType:Incoming_FileTransfer];
1307 [fileTransfer setSize:(xfer->size)];
1310 //accept the request
1311 [purpleThread xferRequestAccepted:xfer withFileName:[fileTransfer localFilename]];
1313 //set the size - must be done after request is accepted?
1316 [fileTransfer setStatus:Accepted_FileTransfer];
1319 //User refused a receive request. Tell purple; we don't release the ESFileTransfer object
1320 //since that will happen when the xfer is destroyed. This will end up calling back on
1321 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
1322 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
1324 PurpleXfer *xfer = [[fileTransfer accountData] pointerValue];
1326 [purpleThread xferRequestRejected:xfer];
1330 //Cancel a file transfer in progress. Tell purple; we don't release the ESFileTransfer object
1331 //since that will happen when the xfer is destroyed. This will end up calling back on
1332 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
1333 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
1335 PurpleXfer *xfer = [[fileTransfer accountData] pointerValue];
1337 [purpleThread xferCancel:xfer];
1341 //Account Connectivity -------------------------------------------------------------------------------------------------
1342 #pragma mark Connect
1343 //Connect this account (Our password should be in the instance variable 'password' all ready for us)
1349 //create a purple account if one does not already exist
1350 [self createNewPurpleAccount];
1351 AILog(@"Created PurpleAccount 0x%x with UID %@ and protocolPlugin %s", account, [self UID], [self protocolPlugin]);
1354 //Make sure our settings are correct
1355 if ([self connectivityBasedOnNetworkReachability] &&
1356 ![[self host] length]) {
1357 //If we use the network for connectivity, and we don't have a host, we need to get ourselves one. Prompt for it!
1358 [self promptForHostBeforeConnecting];
1360 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
1364 static void prompt_host_cancel_cb(CBPurpleAccount *self) {
1369 static void prompt_host_ok_cb(CBPurpleAccount *self, const char *host) {
1371 [self setPreference:[NSString stringWithUTF8String:host]
1372 forKey:KEY_CONNECT_HOST
1373 group:GROUP_ACCOUNT_STATUS];
1375 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
1377 prompt_host_cancel_cb(self);
1381 - (void)promptForHostBeforeConnecting
1383 purple_request_input(NULL, [[NSString stringWithFormat:AILocalizedString(@"%@ (%@) Setup", "first %@ is an account name; second is a service. This is a title for a window"),
1384 [self formattedUID], [[self service] shortDescription]] UTF8String],
1385 [AILocalizedString(@"No Server Specified", nil) UTF8String],
1386 [[NSString stringWithFormat:AILocalizedString(@"No server has been configured for the %@ account %@. Please enter one below to connect", nil),
1387 [[self service] longDescription], [self formattedUID]] UTF8String],
1388 /* default value */ "", /* multiline */ FALSE, /* masked */ FALSE, /* hint */ NULL,
1389 [AILocalizedString(@"Connect", "Button title to connect; this is a verb") UTF8String], G_CALLBACK(prompt_host_ok_cb),
1390 [AILocalizedString(@"Cancel", nil) UTF8String], G_CALLBACK(prompt_host_cancel_cb),
1391 /* account */ NULL, /* who */ NULL, /* conv */ NULL,
1397 - (void)continueConnectWithConfiguredPurpleAccount
1399 //Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1400 [self configureAccountProxyNotifyingTarget:self selector:@selector(continueConnectWithConfiguredProxy)];
1403 - (void)continueConnectWithConfiguredProxy
1405 //Set password and connect
1406 purple_account_set_password(account, [password UTF8String]);
1408 //Set our current status state after filtering its statusMessage as appropriate. This will take us online in the process.
1409 AIStatus *statusState = [self statusObjectForKey:@"StatusState"];
1410 if (!statusState || ([statusState statusType] == AIOfflineStatusType)) {
1411 statusState = [[adium statusController] defaultInitialStatusState];
1414 AILog(@"Adium: Connect: %@ initiating connection using status state %@ (%@).",[self UID],statusState,
1415 [statusState statusMessageString]);
1417 [self autoRefreshingOutgoingContentForStatusKey:@"StatusState"
1418 selector:@selector(gotFilteredStatusMessage:forStatusState:)
1419 context:statusState];
1423 //Make sure our settings are correct; notify target/selector when we're finished
1424 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector
1426 NSInvocation *contextInvocation;
1428 //Perform the synchronous configuration activities (subclasses may want to take action in this function)
1429 [self configurePurpleAccount];
1431 contextInvocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1433 [contextInvocation setTarget:target];
1434 [contextInvocation setSelector:selector];
1435 [contextInvocation retainArguments];
1437 //Set the text profile BEFORE beginning the connect process, to avoid problems with setting it while the
1438 //connect occurs. Once that's done, contextInvocation will be invoked, continuing the configurePurpleAccount process.
1439 [self autoRefreshingOutgoingContentForStatusKey:@"TextProfile"
1440 selector:@selector(setAccountProfileTo:configurePurpleAccountContext:)
1441 context:contextInvocation];
1445 * @brief The server name to be passed to libpurple
1446 * By default, this is the host as seen by the rest of Adium. Subclasses may choose to override this if
1447 * some trickery is desired between what is told to libpurple and what the rest of Adium sees.
1449 - (NSString *)hostForPurple
1454 //Synchronous purple account configuration activites, always performed after an account is created.
1455 //This is a definite subclassing point so prpls can apply their own account settings.
1456 - (void)configurePurpleAccount
1462 hostName = [self hostForPurple];
1463 if (hostName && [hostName length]) {
1464 purple_account_set_string(account, "server", [hostName UTF8String]);
1468 portNumber = [self port];
1470 purple_account_set_int(account, "port", portNumber);
1474 purple_account_set_check_mail(account, [[self shouldCheckMail] boolValue]);
1476 //Update a few status keys before we begin connecting. Libpurple will send these automatically
1477 [self updateStatusForKey:KEY_USER_ICON];
1481 * @brief Configure libpurple's proxy settings using the current system values
1483 * target/selector are used rather than a hardcoded callback (or getProxyConfigurationNotifyingTarget: directly) because this allows code reuse
1484 * between the connect and register processes, which are similar in their need for proxy configuration
1486 - (void)configureAccountProxyNotifyingTarget:(id)target selector:(SEL)selector
1488 NSInvocation *invocation;
1490 //Configure the invocation we will use when we are done configuring
1491 invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1492 [invocation setSelector:selector];
1493 [invocation setTarget:target];
1495 [self getProxyConfigurationNotifyingTarget:self
1496 selector:@selector(retrievedProxyConfiguration:context:)
1497 context:invocation];
1501 * @brief Callback for -[self getProxyConfigurationNotifyingTarget:selector:context:]
1503 - (void)retrievedProxyConfiguration:(NSDictionary *)proxyConfig context:(NSInvocation *)invocation
1505 PurpleProxyInfo *proxy_info;
1507 AdiumProxyType proxyType = [[proxyConfig objectForKey:@"AdiumProxyType"] intValue];
1509 proxy_info = purple_proxy_info_new();
1510 purple_account_set_proxy_info(account, proxy_info);
1512 PurpleProxyType purpleAccountProxyType;
1514 switch (proxyType) {
1515 case Adium_Proxy_HTTP:
1516 case Adium_Proxy_Default_HTTP:
1517 purpleAccountProxyType = PURPLE_PROXY_HTTP;
1519 case Adium_Proxy_SOCKS4:
1520 case Adium_Proxy_Default_SOCKS4:
1521 purpleAccountProxyType = PURPLE_PROXY_SOCKS4;
1523 case Adium_Proxy_SOCKS5:
1524 case Adium_Proxy_Default_SOCKS5:
1525 purpleAccountProxyType = PURPLE_PROXY_SOCKS5;
1527 case Adium_Proxy_None:
1529 purpleAccountProxyType = PURPLE_PROXY_NONE;
1533 purple_proxy_info_set_type(proxy_info, purpleAccountProxyType);
1535 if (proxyType != Adium_Proxy_None) {
1536 purple_proxy_info_set_host(proxy_info, (char *)[[proxyConfig objectForKey:@"Host"] UTF8String]);
1537 purple_proxy_info_set_port(proxy_info, [[proxyConfig objectForKey:@"Port"] intValue]);
1539 purple_proxy_info_set_username(proxy_info, (char *)[[proxyConfig objectForKey:@"Username"] UTF8String]);
1540 purple_proxy_info_set_password(proxy_info, (char *)[[proxyConfig objectForKey:@"Password"] UTF8String]);
1542 AILog(@"Connecting with proxy type %i and proxy host %@",proxyType, [proxyConfig objectForKey:@"Host"]);
1545 [invocation invoke];
1548 //Sublcasses should override to provide a string for each progress step
1549 - (NSString *)connectionStringForStep:(int)step { return nil; };
1552 * @brief Should the account's status be updated as soon as it is connected?
1554 * If YES, the StatusState and IdleSince status keys will be told to update as soon as the account connects.
1555 * This will allow the account to send its status information to the server upon connecting.
1557 * If this information is already known by the account at the time it connects and further prompting to send it is
1558 * not desired, return NO.
1560 * libpurple should already have been told of our status before connecting began.
1562 - (BOOL)updateStatusImmediatelyAfterConnecting
1567 //Our account has connected
1568 - (void)accountConnectionConnected
1570 AILog(@"************ %@ CONNECTED ***********",[self UID]);
1574 [[adium notificationCenter] addObserver:self
1575 selector:@selector(iTunesDidUpdate:)
1576 name:Adium_iTunesTrackChangedNotification
1580 [self silenceAllContactUpdatesForInterval:18.0];
1581 [[adium contactController] delayListObjectNotificationsUntilInactivity];
1583 //Clear any previous disconnection error
1584 [self setLastDisconnectionError:nil];
1587 [purpleThread unregisterAccount:self];
1590 - (void)accountConnectionProgressStep:(NSNumber *)step percentDone:(NSNumber *)connectionProgressPrecent
1592 NSString *connectionProgressString = [self connectionStringForStep:[step intValue]];
1594 [self setStatusObject:connectionProgressString forKey:@"ConnectionProgressString" notify:NO];
1595 [self setStatusObject:connectionProgressPrecent forKey:@"ConnectionProgressPercent" notify:NO];
1598 [self notifyOfChangedStatusSilently:NO];
1600 AILog(@"************ %@ --step-- %i",[self UID],[step intValue]);
1604 * @brief Name to use when creating a PurpleAccount for this CBPurpleAccount
1606 * By default, we just use the formattedUID. Subclasses can override this to provide other handling,
1607 * such as appending @mac.com if necessary for dotMac accounts.
1609 - (const char *)purpleAccountName
1611 return [[self formattedUID] UTF8String];
1614 - (void)createNewPurpleAccount
1616 if (!purpleThread) {
1617 purpleThread = [[SLPurpleCocoaAdapter sharedInstance] retain];
1620 //Create a fresh version of the account
1621 if ((account = purple_account_new([self purpleAccountName], [self protocolPlugin]))) {
1622 [purpleThread addAdiumAccount:self];
1624 AILog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
1625 [self purpleAccountName], [self protocolPlugin]);
1626 NSLog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
1627 [self purpleAccountName], [self protocolPlugin]);
1631 #pragma mark Disconnect
1634 * @brief Disconnect this account
1638 if ([self online] || [self integerStatusObjectForKey:@"Connecting"]) {
1639 //As per AIAccount's documentation, call super's implementation
1642 [[adium contactController] delayListObjectNotificationsUntilInactivity];
1644 //Tell libpurple to disconnect
1645 [purpleThread disconnectAccount:self];
1649 - (void)setLastDisconnectionReason:(PurpleConnectionError)reason
1651 lastDisconnectionReason = reason;
1654 - (PurpleConnectionError)lastDisconnectionReason
1656 return lastDisconnectionReason;
1660 * @brief Our account was unexpectedly disconnected with an error message
1662 - (void)accountConnectionReportDisconnect:(NSString *)text withReason:(PurpleConnectionError)reason
1664 [self setLastDisconnectionError:text];
1665 [self setLastDisconnectionReason:reason];
1667 if (reason == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED)
1668 [self serverReportedInvalidPassword];
1670 //We are disconnecting
1671 [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Disconnecting" notify:NotifyNow];
1673 AILog(@"%@ accountConnectionReportDisconnect: %@",self,lastDisconnectionError);
1676 - (void)accountConnectionNotice:(NSString *)connectionNotice
1678 [[adium interfaceController] handleErrorMessage:[NSString stringWithFormat:AILocalizedString(@"%@ (%@) : Connection Notice",nil),[self formattedUID],[service description]]
1679 withDescription:connectionNotice];
1684 * @brief Our account has disconnected
1686 * This is called after the account disconnects for any reason
1688 - (void)accountConnectionDisconnected
1690 //Clear status objects which don't make sense for a disconnected account
1691 [self setStatusObject:nil forKey:@"TextProfile" notify:NO];
1694 [self notifyOfChangedStatusSilently:NO];
1696 [[adium notificationCenter] removeObserver:self
1697 name:Adium_iTunesTrackChangedNotification
1701 //Report that we disconnected
1702 AILog(@"%@: Telling the core we disconnected", self);
1703 [self didDisconnect];
1706 [super alertForAccountDeletion:deletionDialog didReturn:NSAlertDefaultReturn];
1709 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
1711 AIReconnectDelayType reconnectDelayType;
1713 if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED) {
1714 [self setLastDisconnectionError:AILocalizedString(@"Incorrect username or password","Error message displayed when the server reports username or password as being incorrect.")];
1715 reconnectDelayType = AIReconnectImmediately;
1717 } else if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_INVALID_USERNAME) {
1718 reconnectDelayType = AIReconnectNever;
1719 //Enable this after Adium 1.2, which is in string freeze as it is added.
1720 /* *disconnectionError = AILocalizedString(@"The name you entered is not registered. Check to ensure you typed it correctly.", nil); */
1722 } else if (purple_connection_error_is_fatal([self lastDisconnectionReason])) {
1723 reconnectDelayType = AIReconnectNever;
1726 reconnectDelayType = AIReconnectNormally;
1729 return reconnectDelayType;
1732 #pragma mark Registering
1733 - (void)performRegisterWithPassword:(NSString *)inPassword
1735 //Save the new password
1736 if (password != inPassword) {
1737 [password release]; password = [inPassword retain];
1741 //create a purple account if one does not already exist
1742 [self createNewPurpleAccount];
1743 AILog(@"Registering: created PurpleAccount 0x%x with UID %@, protocolPlugin %s", account, [self UID], [self protocolPlugin]);
1747 [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Connecting" notify:NotifyNow];
1749 //Make sure our settings are correct
1750 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredPurpleAccount)];
1753 - (void)continueRegisterWithConfiguredProxy
1755 //Set password and connect
1756 purple_account_set_password(account, [password UTF8String]);
1758 AILog(@"Adium: Register: %@ initiating connection.",[self UID]);
1760 [purpleThread registerAccount:self];
1763 - (void)continueRegisterWithConfiguredPurpleAccount
1765 //Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1766 [self configureAccountProxyNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredProxy)];
1769 - (void)purpleAccountRegistered:(BOOL)success
1771 if (success && [[self service] accountViewController]) {
1772 NSString *username = (account->username ? [NSString stringWithUTF8String:account->username] : [NSNull null]);
1773 NSString *pw = (account->password ? [NSString stringWithUTF8String:account->password] : [NSNull null]);
1775 [[adium notificationCenter] postNotificationName:AIAccountUsernameAndPasswordRegisteredNotification
1777 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
1778 username, @"username",
1784 //Account Status ------------------------------------------------------------------------------------------------------
1785 #pragma mark Account Status
1786 //Status keys this account supports
1787 - (NSSet *)supportedPropertyKeys
1789 static NSMutableSet *supportedPropertyKeys = nil;
1791 if (!supportedPropertyKeys) {
1792 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
1796 @"DefaultUserIconFilename",
1797 KEY_ACCOUNT_CHECK_MAIL,
1799 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
1803 return supportedPropertyKeys;
1807 - (void)updateStatusForKey:(NSString *)key
1809 [super updateStatusForKey:key];
1811 //Now look at keys which only make sense if we have an account
1813 AILog(@"%@: Updating status for key: %@",self, key);
1815 if ([key isEqualToString:@"IdleSince"]) {
1816 NSDate *idleSince = [self preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
1817 [self setAccountIdleSinceTo:idleSince];
1819 } else if ([key isEqualToString:@"TextProfile"]) {
1820 [self autoRefreshingOutgoingContentForStatusKey:key selector:@selector(setAccountProfileTo:)];
1822 } else if ([key isEqualToString:KEY_USER_ICON]) {
1823 NSData *data = [self userIconData];
1825 [self setAccountUserImageData:data];
1827 } else if ([key isEqualToString:KEY_ACCOUNT_CHECK_MAIL]) {
1828 //Update the mail checking setting if the account is already made (if it isn't, we'll set it when it is made)
1830 [purpleThread setCheckMail:[self shouldCheckMail]
1838 * @brief Return the purple status type to be used for a status
1840 * Most subclasses should override this method; these generic values may be appropriate for others.
1842 * Active services provided nonlocalized status names. An AIStatus is passed to this method along with a pointer
1843 * to the status message. This method should handle any status whose statusNname this service set as well as any statusName
1844 * defined in AIStatusController.h (which will correspond to the services handled by Adium by default).
1845 * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
1846 * [statusState statusType] for a general idea of the status's type.
1848 * @param statusState The status for which to find the purple status ID
1849 * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
1851 * @result The purple status ID
1853 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
1854 arguments:(NSMutableDictionary *)arguments
1856 char *statusID = NULL;
1858 switch ([statusState statusType]) {
1859 case AIAvailableStatusType:
1860 statusID = "available";
1862 case AIAwayStatusType:
1866 case AIInvisibleStatusType:
1867 statusID = "invisible";
1870 case AIOfflineStatusType:
1871 statusID = "offline";
1878 - (BOOL)shouldAddMusicalNoteToNowPlayingStatus
1883 - (BOOL)shouldSetITMSLinkForNowPlayingStatus
1888 - (BOOL)shouldIncludeNowPlayingInformationInAllStatuses
1893 - (void)iTunesDidUpdate:(NSNotification*)notification {
1894 if ([self shouldIncludeNowPlayingInformationInAllStatuses]) {
1896 tuneinfo = [[notification object] retain];
1898 // update info in prpl
1900 [self updateStatusForKey:@"StatusState"];
1905 * @brief Perform the setting of a status state
1907 * Sets the account to a passed status state. The account should set itself to best possible status given the return
1908 * values of statusState's accessors. The passed statusMessage has been filtered; it should be used rather than
1909 * [statusState statusMessage], which returns an unfiltered statusMessage.
1911 * @param statusState The state to enter
1912 * @param statusMessage The filtered status message to use.
1914 - (void)setStatusState:(AIStatus *)statusState usingStatusMessage:(NSAttributedString *)statusMessage
1916 NSString *encodedStatusMessage;
1917 NSMutableDictionary *arguments = [[NSMutableDictionary alloc] init];
1919 //Get the purple status type from this class or subclasses, which may also potentially modify or nullify our statusMessage
1920 const char *statusID = [self purpleStatusIDForStatus:statusState
1921 arguments:arguments];
1923 if (![statusMessage length] &&
1924 ([statusState statusType] == AIAwayStatusType) &&
1925 [statusState statusName] &&
1926 (!statusID || (strcmp(statusID, "away") == 0))) {
1927 /* If we don't have a status message, and the status type is away for a non-default away such as "Do Not Disturb", and we're only setting
1928 * a default away state becuse we don't know a better one for this service, get a default
1929 * description of this away state. This allows, for example, an AIM user to set the "Do Not Disturb" type provided by her ICQ account
1930 * and have the away message be set appropriately.
1932 statusMessage = [NSAttributedString stringWithString:[[adium statusController] descriptionForStateOfStatus:statusState]];
1935 BOOL isNowPlayingStatus = ([statusState specialStatusType] == AINowPlayingSpecialStatusType);
1936 if (isNowPlayingStatus && [statusMessage length]) {
1937 if ([self shouldAddMusicalNoteToNowPlayingStatus]) {
1938 #define MUSICAL_NOTE_AND_SPACE [NSString stringWithUTF8String:"\xe2\x99\xab "]
1939 NSMutableAttributedString *temporaryStatusMessage;
1940 temporaryStatusMessage = [[[NSMutableAttributedString alloc] initWithString:MUSICAL_NOTE_AND_SPACE] autorelease];
1941 [temporaryStatusMessage appendAttributedString:statusMessage];
1943 statusMessage = temporaryStatusMessage;
1946 if ([self shouldSetITMSLinkForNowPlayingStatus]) {
1947 //Grab the message's subtext, which is the song link if we're using the Current iTunes Track status
1948 NSString *itmsStoreLink = [statusMessage attribute:@"AIMessageSubtext" atIndex:0 effectiveRange:NULL];
1949 if (itmsStoreLink) {
1950 [arguments setObject:itmsStoreLink
1956 if (isNowPlayingStatus || [self shouldIncludeNowPlayingInformationInAllStatuses]) {
1957 if (tuneinfo && [[tuneinfo objectForKey:ITUNES_PLAYER_STATE] isEqualToString:@"Playing"]) {
1958 NSString *artist = [tuneinfo objectForKey:ITUNES_ARTIST];
1959 NSString *name = [tuneinfo objectForKey:ITUNES_NAME];
1961 [arguments setObject:(artist ? artist : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ARTIST]];
1962 [arguments setObject:(name ? name : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TITLE]];
1963 [arguments setObject:([tuneinfo objectForKey:ITUNES_ALBUM] ? [tuneinfo objectForKey:ITUNES_ALBUM] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ALBUM]];
1964 [arguments setObject:([tuneinfo objectForKey:ITUNES_GENRE] ? [tuneinfo objectForKey:ITUNES_GENRE] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_GENRE]];
1965 [arguments setObject:([tuneinfo objectForKey:ITUNES_TOTAL_TIME] ? [tuneinfo objectForKey:ITUNES_TOTAL_TIME]:[NSNumber numberWithInt:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TIME]];
1966 [arguments setObject:([tuneinfo objectForKey:ITUNES_YEAR] ? [tuneinfo objectForKey:ITUNES_YEAR]:[NSNumber numberWithInt:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_YEAR]];
1967 [arguments setObject:([tuneinfo objectForKey:ITUNES_STORE_URL] ? [tuneinfo objectForKey:ITUNES_STORE_URL] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_URL]];
1969 [arguments setObject:[NSString stringWithFormat:@"%@%@%@", (name ? name : @""), (name && artist ? @" - " : @""), (artist ? artist : @"")]
1970 forKey:[NSString stringWithUTF8String:PURPLE_TUNE_FULL]];
1973 [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ARTIST]];
1974 [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TITLE]];
1975 [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ALBUM]];
1976 [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_GENRE]];
1977 [arguments setObject:[NSNumber numberWithInt:-1] forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TIME]];
1978 [arguments setObject:[NSNumber numberWithInt:-1] forKey:[NSString stringWithUTF8String:PURPLE_TUNE_YEAR]];
1979 [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_URL]];
1980 [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_FULL]];
1984 //Encode the status message if we have one
1985 encodedStatusMessage = (statusMessage ?
1986 [self encodedAttributedString:statusMessage
1987 forStatusState:statusState] :
1989 if (encodedStatusMessage) {
1990 [arguments setObject:encodedStatusMessage
1994 [self setStatusState:statusState
1996 isActive:[NSNumber numberWithBool:YES] /* We're only using exclusive states for now... I hope. */
1997 arguments:arguments];
1999 [arguments release];
2003 * @brief Perform the actual setting of a state
2005 * This is called by setStatusState. It allows subclasses to perform any other behaviors, such as modifying a display
2006 * name, which are called for by the setting of the state; most of the processing has already been done, however, so
2007 * most subclasses will not need to implement this.
2009 * @param statusState The AIStatus which is being set
2010 * @param statusID The Purple-sepcific statusID we are setting
2011 * @param isActive An NSNumber with a bool YES if we are activating (going to) the passed state, NO if we are deactivating (going away from) the passed state.
2012 * @param arguments Purple-specific arguments specified by the account. It must contain only NSString objects and keys.
2014 - (void)setStatusState:(AIStatus *)statusState statusID:(const char *)statusID isActive:(NSNumber *)isActive arguments:(NSMutableDictionary *)arguments
2016 [purpleThread setStatusID:statusID
2022 //Set our idle (Pass nil for no idle)
2023 - (void)setAccountIdleSinceTo:(NSDate *)idleSince
2025 [purpleThread setIdleSinceTo:idleSince onAccount:self];
2027 //We now should update our idle status object
2028 [self setStatusObject:([idleSince timeIntervalSinceNow] ? idleSince : nil)
2033 //Set the profile, then invoke the passed invocation to return control to the target/selector specified
2034 //by a configurePurpleAccountNotifyingTarget:selector: call.
2035 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation
2037 [self setAccountProfileTo:profile];
2039 [inInvocation invoke];
2042 //Set our profile immediately on the purpleThread
2043 - (void)setAccountProfileTo:(NSAttributedString *)profile
2045 if (!profile || ![[profile string] isEqualToString:[[self statusObjectForKey:@"TextProfile"] string]]) {
2046 NSString *profileHTML = nil;
2048 //Convert the profile to HTML, and pass it to libpurple
2050 profileHTML = [self encodedAttributedString:profile forListObject:nil];
2053 [purpleThread setInfo:profileHTML onAccount:self];
2055 //We now have a profile
2056 [self setStatusObject:profile forKey:@"TextProfile" notify:NotifyNow];
2061 * @brief Set our user image
2063 * Pass nil for no image. This resizes and converts the image as needed for our protocol.
2064 * After setting it with purple, it sets it within Adium; if this is not called, the image will
2065 * show up neither locally nor remotely.
2067 - (void)setAccountUserImageData:(NSData *)originalData
2069 NSImage *image = (originalData ? [[[NSImage alloc] initWithData:originalData] autorelease] : nil);
2072 NSSize imageSize = [image size];
2074 //Clear the existing icon first
2075 [purpleThread setBuddyIcon:nil onAccount:self];
2077 /* Now pass libpurple the new icon. Libpurple takes icons as a file, so we save our
2078 * image to one, and then pass libpurple the path. Check to be sure our image doesn't have an NSZeroSize size,
2079 * which would indicate currupt data */
2080 if (image && !NSEqualSizes(NSZeroSize, imageSize)) {
2082 PurplePluginProtocolInfo *prpl_info = ((prpl = purple_find_prpl(account->protocol_id)) ?
2083 PURPLE_PLUGIN_PROTOCOL_INFO(prpl) :
2086 AILog(@"Original image of size %f %f",imageSize.width,imageSize.height);
2088 if (prpl_info && (prpl_info->icon_spec.format)) {
2089 NSData *buddyIconData = nil;
2090 BOOL smallEnough, prplScales;
2093 /* We need to scale it down if:
2094 * 1) The prpl needs to scale before it sends to the server or other buddies AND
2095 * 2) The image is larger than the maximum size allowed by the protocol
2096 * We ignore the minimum required size, as scaling up just leads to pixellated images.
2098 smallEnough = (prpl_info->icon_spec.max_width >= imageSize.width &&
2099 prpl_info->icon_spec.max_height >= imageSize.height);
2101 prplScales = (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_SEND) || (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_DISPLAY);
2103 if (prplScales && !smallEnough) {
2104 int width = imageSize.width;
2105 int height = imageSize.height;
2107 purple_buddy_icon_get_scale_size(&prpl_info->icon_spec, &width, &height);
2108 //Determine the scaled size. If it's too big, scale to the largest permissable size
2109 image = [image imageByScalingToSize:NSMakeSize(width, height)];
2111 /* Our original data is no longer valid, since we had to scale to a different size */
2113 AILog(@"%@: Scaled image to size %@", self, NSStringFromSize([image size]));
2116 if (!buddyIconData) {
2117 char **prpl_formats = g_strsplit(prpl_info->icon_spec.format,",",0);
2119 //Look for gif first if the image is animated
2120 NSImageRep *imageRep = [image bestRepresentationForDevice:nil] ;
2121 if ([imageRep isKindOfClass:[NSBitmapImageRep class]] &&
2122 [[(NSBitmapImageRep *)imageRep valueForProperty:NSImageFrameCount] intValue] > 1) {
2124 for (i = 0; prpl_formats[i]; i++) {
2125 if (strcmp(prpl_formats[i],"gif") == 0) {
2126 /* Try to use our original data. If we had to scale, originalData will have been set
2127 * to nil and we'll continue below to convert the image. */
2128 AILog(@"l33t script kiddie animated GIF!!111");
2130 buddyIconData = originalData;
2137 if (!buddyIconData) {
2138 for (i = 0; prpl_formats[i]; i++) {
2139 if (strcmp(prpl_formats[i],"png") == 0) {
2140 buddyIconData = [image PNGRepresentation];
2144 } else if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
2145 buddyIconData = [image JPEGRepresentationWithCompressionFactor:1.0];
2149 } else if ((strcmp(prpl_formats[i],"tiff") == 0) || (strcmp(prpl_formats[i],"tif") == 0)) {
2150 buddyIconData = [image TIFFRepresentation];
2154 } else if (strcmp(prpl_formats[i],"gif") == 0) {
2155 buddyIconData = [image GIFRepresentation];
2159 } else if (strcmp(prpl_formats[i],"bmp") == 0) {
2160 buddyIconData = [image BMPRepresentation];
2167 size_t maxSize = prpl_info->icon_spec.max_filesize;
2168 if (maxSize > 0 && ([buddyIconData length] > maxSize)) {
2169 AILog(@"Image %i is larger than %i!",[buddyIconData length],maxSize);
2170 for (i = 0; prpl_formats[i]; i++) {
2171 if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
2172 float compressionFactor;
2173 for (compressionFactor = 0.99; compressionFactor > 0.4; compressionFactor -= 0.01) {
2174 buddyIconData = [image JPEGRepresentationWithCompressionFactor:compressionFactor];
2176 if (buddyIconData && ([buddyIconData length] <= maxSize)) {
2177 AILog(@"Succeeded getting it down to %i with compressionFactor %f",[buddyIconData length],compressionFactor);
2186 g_strfreev(prpl_formats);
2189 [purpleThread setBuddyIcon:buddyIconData onAccount:self];
2194 //We now have an icon
2195 [self setStatusObject:image forKey:KEY_USER_ICON notify:NotifyNow];
2198 #pragma mark Group Chat
2199 - (BOOL)inviteContact:(AIListContact *)inContact toChat:(AIChat *)inChat withMessage:(NSString *)inviteMessage
2201 [purpleThread inviteContact:inContact toChat:inChat withMessage:inviteMessage];
2206 #pragma mark Buddy Menu Items
2207 //Action of a dynamically-generated contact menu item
2208 - (void)performContactMenuAction:(NSMenuItem *)sender
2210 NSDictionary *dict = [sender representedObject];
2212 [purpleThread performContactMenuActionFromDict:dict forAccount:self];
2216 * @brief Utility method when generating buddy-specific menu items
2218 * Adds the menu item for act to a growing array of NSMenuItems. If act has children (a submenu), this method is used recursively
2219 * to generate the submenu containing each child menu item.
2221 - (void)addMenuItemForMenuAction:(PurpleMenuAction *)act forListContact:(AIListContact *)inContact purpleBuddy:(PurpleBuddy *)buddy toArray:(NSMutableArray *)menuItemArray withServiceIcon:(NSImage *)serviceIcon
2224 NSMenuItem *menuItem;
2227 //If titleForContactMenuLabel:forContact: returns nil, we don't add the menuItem
2230 (title = [self titleForContactMenuLabel:act->label
2231 forContact:inContact])) {
2232 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2234 action:@selector(performContactMenuAction:)
2236 [menuItem setImage:serviceIcon];
2239 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2240 [NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
2241 /* act->data may be freed by purple_menu_action_free() before we use it, I'm afraid... */
2242 [NSValue valueWithPointer:act->data],@"PurpleMenuActionData",
2243 [NSValue valueWithPointer:buddy],@"PurpleBuddy",
2246 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2247 [NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
2248 [NSValue valueWithPointer:buddy],@"PurpleBuddy",
2252 [menuItem setRepresentedObject:dict];
2254 //If there is a submenu, generate and set it
2255 if (act->children) {
2256 NSMutableArray *childrenArray = [NSMutableArray array];
2258 //Add a NSMenuItem for each child
2259 for (l = ll = act->children; l; l = l->next) {
2260 [self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
2261 forListContact:inContact
2263 toArray:childrenArray
2264 withServiceIcon:serviceIcon];
2266 g_list_free(act->children);
2268 if ([childrenArray count]) {
2269 NSEnumerator *enumerator = [childrenArray objectEnumerator];
2270 NSMenuItem *childMenuItem;
2271 NSMenu *submenu = [[NSMenu alloc] init];
2273 while ((childMenuItem = [enumerator nextObject])) {
2274 [submenu addItem:childMenuItem];
2277 [menuItem setSubmenu:submenu];
2282 [menuItemArray addObject:menuItem];
2286 purple_menu_action_free(act);
2289 //Returns an array of menuItems specific for this contact based on its account and potentially status
2290 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
2292 NSMutableArray *menuItemArray = nil;
2294 if (account && purple_account_is_connected(account)) {
2296 PurplePluginProtocolInfo *prpl_info = ((prpl = purple_find_prpl(account->protocol_id)) ?
2297 PURPLE_PLUGIN_PROTOCOL_INFO(prpl) :
2302 //Find the PurpleBuddy
2303 buddy = purple_find_buddy(account, [[inContact UID] UTF8String]);
2305 if (prpl_info && prpl_info->blist_node_menu && buddy) {
2306 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:[self service]
2307 type:AIServiceIconSmall
2308 direction:AIIconNormal];
2310 menuItemArray = [NSMutableArray array];
2312 //Add a NSMenuItem for each node action specified by the prpl
2313 for (l = ll = prpl_info->blist_node_menu((PurpleBlistNode *)buddy); l; l = l->next) {
2314 [self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
2315 forListContact:inContact
2317 toArray:menuItemArray
2318 withServiceIcon:serviceIcon];
2322 //Don't return an empty array
2323 if (![menuItemArray count]) menuItemArray = nil;
2327 return menuItemArray;
2330 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2331 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
2333 return [NSString stringWithUTF8String:label];
2337 * @brief Menu items for the account's actions
2339 * Returns an array of menu items for account-specific actions. This is the best place to add protocol-specific
2340 * actions that aren't otherwise supported by Adium. It will only be queried if the account is online.
2341 * @return NSArray of NSMenuItem instances for this account
2343 - (NSArray *)accountActionMenuItems
2345 NSMutableArray *menuItemArray = nil;
2347 if (account && purple_account_is_connected(account)) {
2348 PurplePlugin *plugin = account->gc->prpl;
2350 if (PURPLE_PLUGIN_HAS_ACTIONS(plugin)) {
2353 actions = PURPLE_PLUGIN_ACTIONS(plugin, account->gc);
2355 //Avoid adding separators between nonexistant items (i.e. items which Purple shows but we don't)
2356 BOOL addedAnAction = NO;
2357 for (l = actions; l; l = l->next) {
2360 PurplePluginAction *action;
2362 NSMenuItem *menuItem;
2365 action = (PurplePluginAction *) l->data;
2367 //If titleForAccountActionMenuLabel: returns nil, we don't add the menuItem
2370 (title = [self titleForAccountActionMenuLabel:action->label])) {
2372 action->plugin = plugin;
2373 action->context = account->gc;
2375 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2377 action:@selector(performAccountMenuAction:)
2378 keyEquivalent:@""] autorelease];
2379 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2380 [NSValue valueWithPointer:action->callback], @"PurplePluginActionCallback",
2381 [NSValue valueWithPointer:action->user_data], @"PurplePluginActionCallbackUserData",
2384 [menuItem setRepresentedObject:dict];
2386 if (!menuItemArray) menuItemArray = [NSMutableArray array];
2388 [menuItemArray addObject:menuItem];
2389 addedAnAction = YES;
2392 purple_plugin_action_free(action);
2395 if (addedAnAction) {
2396 [menuItemArray addObject:[NSMenuItem separatorItem]];
2402 g_list_free(actions);
2406 return menuItemArray;
2409 //Action of a dynamically-generated contact menu item
2410 - (void)performAccountMenuAction:(NSMenuItem *)sender
2412 NSDictionary *dict = [sender representedObject];
2414 [purpleThread performAccountMenuActionFromDict:dict forAccount:self];
2417 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2418 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
2420 if ((strcmp(label, _("Change Password...")) == 0) || (strcmp(label, _("Change Password")) == 0)) {
2421 return [[NSString stringWithFormat:AILocalizedString(@"Change Password", "Menu item title for changing the password of an account")] stringByAppendingEllipsis];
2423 return [NSString stringWithUTF8String:label];
2427 /********************************/
2428 /* AIAccount subclassed methods */
2429 /********************************/
2430 #pragma mark AIAccount Subclassed Methods
2433 NSDictionary *defaults = [NSDictionary dictionaryNamed:[NSString stringWithFormat:@"PurpleDefaults%@",[[self service] serviceID]]
2434 forClass:[self class]];
2437 [[adium preferenceController] registerDefaults:defaults
2438 forGroup:GROUP_ACCOUNT_STATUS
2441 AILog(@"Failed to load defaults for %@",[NSString stringWithFormat:@"PurpleDefaults%@",[[self service] serviceID]]);
2445 [self setLastDisconnectionError:nil];
2447 permittedContactsArray = [[NSMutableArray alloc] init];
2448 deniedContactsArray = [[NSMutableArray alloc] init];
2450 //We will create a purpleAccount the first time we attempt to connect
2453 //Observe preferences changes
2454 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_ALIASES];
2458 * @brief The account will be deleted, we should ask the user for confirmation. If the prpl supports it, we can also remove
2459 * the account from the server (if the user wants us to do that)
2461 - (NSAlert*)alertForAccountDeletion
2464 PurplePluginProtocolInfo *prpl_info;
2466 if (!purpleThread) {
2467 purpleThread = [[SLPurpleCocoaAdapter sharedInstance] retain];
2470 prpl = purple_find_prpl([self protocolPlugin]);
2473 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
2476 if(prpl_info->unregister_user)
2477 return [NSAlert alertWithMessageText:AILocalizedString(@"Delete Account",nil)
2478 defaultButton:AILocalizedString(@"Delete",nil)
2479 alternateButton:AILocalizedString(@"Cancel",nil)
2480 otherButton:AILocalizedString(@"Delete & Unregister",nil)
2481 informativeTextWithFormat:AILocalizedString(@"Delete the account %@? You can also optionally unregister the account on the server if possible.",nil), ([[self formattedUID] length] ? [self formattedUID] : NEW_ACCOUNT_DISPLAY_TEXT)];
2483 return [super alertForAccountDeletion];
2486 - (void)alertForAccountDeletion:(id<AIAccountControllerRemoveConfirmationDialog>)dialog didReturn:(int)returnCode
2489 PurplePluginProtocolInfo *prpl_info;
2491 if (!purpleThread) {
2492 purpleThread = [[SLPurpleCocoaAdapter sharedInstance] retain];
2495 prpl = purple_find_prpl([self protocolPlugin]);
2497 [super alertForAccountDeletion:dialog didReturn:NSAlertAlternateReturn];
2501 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
2503 [super alertForAccountDeletion:dialog didReturn:NSAlertAlternateReturn];
2507 /* If the user canceled, we can tell the superclass immediately.
2508 * If the deletion is in fact happening, we first have to unregister and disconnect.
2509 * This is an asynchronous process.
2511 if(prpl_info->unregister_user) {
2512 switch(returnCode) {
2513 case NSAlertOtherReturn: // delete & unregister
2514 deletionDialog = dialog;
2515 if(!account || !purple_account_is_connected(account)) {
2516 password = [[[adium accountController] passwordForAccount:self] retain];
2519 [purpleThread unregisterAccount:self];
2520 // further progress happens in -unregisteredAccount:
2522 case NSAlertDefaultReturn: // delete
2523 willBeDeleted = YES;
2524 if(!account || !purple_account_is_connected(account)) {
2525 [super alertForAccountDeletion:dialog didReturn:NSAlertDefaultReturn];
2527 deletionDialog = dialog;
2528 [self setShouldBeOnline:NO];
2529 // further progress happens in -accountConnectionDisconnected
2533 [super alertForAccountDeletion:dialog didReturn:NSAlertAlternateReturn];
2537 switch(returnCode) {
2538 case NSAlertDefaultReturn:
2539 willBeDeleted = YES;
2540 if (!account || !purple_account_is_connected(account)) {
2541 [super alertForAccountDeletion:dialog didReturn:NSAlertDefaultReturn];
2543 deletionDialog = dialog;
2544 [self setShouldBeOnline:NO];
2545 // further progress happens in -accountConnectionDisconnected
2549 [super alertForAccountDeletion:dialog didReturn:returnCode];
2554 - (void)unregisteredAccount:(BOOL)success {
2556 willBeDeleted = YES;
2557 NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(setShouldBeOnline:)]];
2558 [inv setTarget:self];
2559 [inv setSelector:@selector(setShouldBeOnline:)];
2560 static BOOL nope = NO;
2561 [inv setArgument:&nope atIndex:2];
2562 [inv performSelector:@selector(invoke) withObject:nil afterDelay:0.0];
2563 // further progress happens in -accountConnectionDisconnected
2566 [super alertForAccountDeletion:deletionDialog didReturn:NSAlertAlternateReturn];
2567 deletionDialog = nil;
2572 * @brief The account's UID changed
2574 - (void)didChangeUID
2576 //Only need to take action if we have a created PurpleAccount already
2577 if (account != NULL) {
2578 //Remove our current account
2579 [purpleThread removeAdiumAccount:self];
2581 //Clear the reference to the PurpleAccount... it'll be created when needed
2588 [[adium preferenceController] unregisterPreferenceObserver:self];
2590 [permittedContactsArray release];
2591 [deniedContactsArray release];
2596 - (NSString *)unknownGroupName {
2597 return (@"Unknown");
2600 - (NSDictionary *)defaultProperties { return [NSDictionary dictionary]; }
2602 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
2604 return [self encodedAttributedString:inAttributedString forListObject:nil];
2607 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
2608 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
2610 [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
2612 if ([group isEqualToString:PREF_GROUP_ALIASES]) {
2613 //If the notification object is a listContact belonging to this account, update the serverside information
2614 if ((account != nil) &&
2615 ([self shouldSetAliasesServerside]) &&
2616 ([key isEqualToString:@"Alias"])) {
2618 NSString *alias = [object preferenceForKey:@"Alias"
2619 group:PREF_GROUP_ALIASES
2620 ignoreInheritedValues:YES];
2622 if ([object isKindOfClass:[AIMetaContact class]]) {
2623 NSEnumerator *enumerator = [[(AIMetaContact *)object containedObjects] objectEnumerator];
2624 AIListContact *containedListContact;
2625 while ((containedListContact = [enumerator nextObject])) {
2626 if ([containedListContact account] == self) {
2627 [purpleThread setAlias:alias forUID:[containedListContact UID] onAccount:self];
2631 } else if ([object isKindOfClass:[AIListContact class]]) {
2632 if ([(AIListContact *)object account] == self) {
2633 [purpleThread setAlias:alias forUID:[object UID] onAccount:self];
2640 #pragma mark Actions for chats
2643 * @name Actions for chats
2644 * @brief This method returns an NSMenu containing the
2645 * actions that are allowed for a given chat.
2646 * @params An AIChat for which the commands are fetched
2647 * @return NSMenu with the proper commands
2649 - (NSMenu*)actionsForChat:(AIChat*)chat
2651 NSMenu *actionsMenu = [[NSMenu alloc] initWithTitle:@"commandsmenu"];
2652 PurpleConversation *conv = existingConvLookupFromChat(chat);
2654 GList *list = purple_cmd_list(conv);
2657 for (l = list; l != NULL; l = l->next) {
2658 const char *cmdName = l->data;
2660 GList *cmdDescription = purple_cmd_help(conv, cmdName);
2661 NSString *name = [NSString stringWithUTF8String:cmdName];
2662 NSString *menuTitle;
2663 if (cmdDescription && cmdDescription->data)
2664 menuTitle = [[AIHTMLDecoder decodeHTML:[NSString stringWithUTF8String:cmdDescription->data]] string];
2668 [actionsMenu addItemWithTitle:menuTitle
2670 action:@selector(doCommand:)
2672 representedObject:[NSDictionary dictionaryWithObjectsAndKeys:
2673 chat, @"associatedChat",
2674 name, @"commandName",
2680 return [actionsMenu autorelease];
2683 -(void)doCommand:(id)sender
2685 NSDictionary *dict = [sender representedObject];
2686 [self verifyCommand:[dict objectForKey:@"commandName"]
2687 forChat:[dict objectForKey:@"associatedChat"]];
2690 -(void)executeCommandWithParameters:(NSMutableDictionary*)parameters
2692 BOOL result = [purpleThread doCommand:[parameters objectForKey:@"totalCommandString"] fromAccount:[parameters objectForKey:@"account"] inChat:[parameters objectForKey:@"chat"]];
2693 if(result == FALSE) {
2695 int choice = NSRunAlertPanel(@"Command Failed!",@"command failed: %@ from Account: %@ in Chat: %@",@"Cancel",@"OK",nil,[parameters objectForKey:@"totalCommandString"],[parameters objectForKey:@"account"],[parameters objectForKey:@"chat"]);
2700 - (BOOL)validateMenuItem:(NSMenuItem *)item
2706 /***************************/
2707 /* Account private methods */
2708 /***************************/
2709 #pragma mark Private
2710 - (void)setTypingFlagOfChat:(AIChat *)chat to:(NSNumber *)typingStateNumber
2712 AITypingState currentTypingState = [chat integerStatusObjectForKey:KEY_TYPING];
2713 AITypingState newTypingState = [typingStateNumber intValue];
2715 if (currentTypingState != newTypingState) {
2716 [chat setStatusObject:(newTypingState ? typingStateNumber : nil)
2722 - (NSNumber *)shouldCheckMail
2724 return [self preferenceForKey:KEY_ACCOUNT_CHECK_MAIL group:GROUP_ACCOUNT_STATUS];
2727 - (BOOL)shouldSetAliasesServerside
2732 - (NSString *)internalObjectID
2734 return [super internalObjectID];